package jp.dodododo.dao.object;

import static jp.dodododo.janerics.GenericsTypeUtil.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jp.dodododo.dao.annotation.Internal;
import jp.dodododo.dao.exception.IllegalPropertyRuntimeException;
import jp.dodododo.dao.message.Message;
import jp.dodododo.dao.types.JavaType;
import jp.dodododo.dao.util.TypesUtil;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

@Internal
public class PropertyDesc implements AnnotatedElement {

	private static final Log logger = LogFactory.getLog(PropertyDesc.class);

	private String propertyName;

	private Class<?> propertyType;

	private Type genericPropertyType;

	private Method readMethod;

	private Method writeMethod;

	private Field field;

	private ObjectDesc objectDesc;

	private boolean readable = false;

	private boolean writable = false;

	private PropertyDesc(String propertyName, ObjectDesc objectDesc) {
		if (propertyName == null) {
			throw new NullPointerException("propertyName");
		}
		this.propertyName = propertyName;
		this.objectDesc = objectDesc;
	}

	protected PropertyDesc(String propertyName, Method readMethod, Method writeMethod, ObjectDesc objectDesc) {
		this(propertyName, objectDesc);
		setReadMethod(readMethod);
		setWriteMethod(writeMethod);
	}

	protected PropertyDesc(String propertyName, Field f, ObjectDesc objectDesc) {
		this(propertyName, objectDesc);
		this.field = f;
		setPropertyType(f.getType());
		this.genericPropertyType = f.getGenericType();

		if (Modifier.isPublic(f.getModifiers()) == true) {
			this.readable = true;
			this.writable = true;
		}
	}

	public String getPropertyName() {
		return propertyName;
	}

	public Class<?> getPropertyType() {
		return propertyType;
	}

	public Method getReadMethod() {
		return readMethod;
	}

	public AccessibleObject getReadAccessibleObject() {
		if (isReadable() == false) {
			return null;
		}
		if (readMethod != null) {
			return readMethod;
		} else {
			return field;
		}
	}

	protected void setReadMethod(Method readMethod) {
		this.readMethod = readMethod;
		if (readMethod != null) {
			setPropertyType(readMethod.getReturnType());
			this.genericPropertyType = readMethod.getGenericReturnType();
			readable = true;
		}
	}

	public boolean hasReadMethod() {
		return readMethod != null;
	}

	public Method getWriteMethod() {
		return writeMethod;
	}

	public AccessibleObject getWriteAccessibleObject() {
		if (isWritable() == false) {
			return null;
		}
		if (writeMethod != null) {
			return writeMethod;
		} else {
			return field;
		}
	}

	protected void setWriteMethod(Method writeMethod) {
		this.writeMethod = writeMethod;
		if (writeMethod != null) {
			setPropertyType(writeMethod.getParameterTypes()[0]);
			this.genericPropertyType = writeMethod.getGenericParameterTypes()[0];
			writable = true;
		}
	}

	public boolean hasWriteMethod() {
		return writeMethod != null;
	}

	public boolean isReadable() {
		return readable;
	}

	public boolean isWritable() {
		return writable;
	}

	public <VALUE> VALUE getValue(Object target) {
		if (!readable) {
			throw new IllegalStateException(Message.getMessage("00008", propertyName, "readable"));
		}
		try {
			if (readMethod == null) {
				@SuppressWarnings("unchecked")
				VALUE ret = (VALUE) field.get(target);
				return ret;
			}
			@SuppressWarnings("unchecked")
			VALUE ret = (VALUE) readMethod.invoke(target, (Object[]) null);
			return ret;
		} catch (Throwable t) {
			throw new IllegalPropertyRuntimeException(objectDesc.getTargetClass(), propertyName, t);
		}
	}

	public void setValue(Object target, Object value) {
		if (writable == false) {
			throw new IllegalStateException(Message.getMessage("00008", propertyName, "writable"));
		}
		try {
			Class<?> propertyType = getPropertyType();
			if (propertyType.isPrimitive()) {
				propertyType = toWrapperType(propertyType);
			}
			if (value != null && (propertyType.isInstance(value) == false)) {
				value = convert(value, propertyType);
			}
			if (writeMethod == null) {
				field.set(target, value);
				return;
			}
			writeMethod.invoke(target, new Object[] { value });
		} catch (Throwable t) {
			throw new IllegalPropertyRuntimeException(objectDesc.getTargetClass(), propertyName, value, t);
		}
	}

	public <V> boolean addValue(Object target, V value) {
		if (isCollectionType() == false) {
			throw new UnsupportedOperationException();
		}
		Collection<V> collection = getValue(target);
		if (collection == null) {
			collection = new ArrayList<V>();
			setValue(target, collection);
		}
		return collection.add(value);
	}

	public <K, V> V putValue(Object target, K key, V value) {
		if (isMapType() == false) {
			throw new UnsupportedOperationException();
		}
		Map<K, V> map = getValue(target);
		if (map == null) {
			map = new HashMap<K, V>();
			setValue(target, map);
		}
		return map.put(key, value);
	}

	private static final Map<Class<?>, Class<?>> wrapperTypes = new HashMap<Class<?>, Class<?>>();

	static {
		wrapperTypes.put(boolean.class, Boolean.class);
		wrapperTypes.put(byte.class, Byte.class);
		wrapperTypes.put(short.class, Short.class);
		wrapperTypes.put(int.class, Integer.class);
		wrapperTypes.put(long.class, Long.class);
		wrapperTypes.put(float.class, Float.class);
		wrapperTypes.put(double.class, Double.class);
		wrapperTypes.put(char.class, Character.class);
	}

	private Class<?> toWrapperType(Class<?> primitiveType) {
		return wrapperTypes.get(primitiveType);
	}

	private Object convert(Object value, Class<?> dest) {
		if (value == null) {
			return null;
		}
		JavaType<?> javaType = TypesUtil.getJavaType(dest);
		return javaType.convert(value);
	}

	public ObjectDesc getObjectDesc() {
		return objectDesc;
	}

	@Override
	public String toString() {
		return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
	}

	@Override
	public boolean equals(Object o) {
		return EqualsBuilder.reflectionEquals(this, o);
	}

	@Override
	public int hashCode() {
		return HashCodeBuilder.reflectionHashCode(this);
	}

	protected void setPropertyType(Class<?> propertyType) {
		if (this.propertyType != null && this.propertyType.equals(propertyType) == false) {
			logger.warn(Message.getMessage("00019", objectDesc.getTargetClass().getName(), getPropertyName()));
		}
		this.propertyType = propertyType;
	}

	public Type getGenericPropertyType() {
		return genericPropertyType;
	}

	public boolean isCollectionType() {
		return isTypeOf(getPropertyType(), Collection.class);
	}

	public boolean isMapType() {
		return isTypeOf(getPropertyType(), Map.class);
	}

	public boolean isEnumType() {
		return getPropertyType().isEnum();
	}

	public <T extends Annotation> T getAnnotation(Class<T> annotation) {
		return getAnnotatedElement().getAnnotation(annotation);
	}

	public Annotation[] getAnnotations() {
		return getAnnotatedElement().getAnnotations();
	}

	public Annotation[] getDeclaredAnnotations() {
		return getAnnotatedElement().getDeclaredAnnotations();
	}

	public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
		return getAnnotatedElement().isAnnotationPresent(annotationClass);
	}

	private AnnotatedElement getAnnotatedElement() {
		return new AnnotatedElementImpl(this.readMethod, this.writeMethod, this.field);
	}

	static class AnnotatedElementImpl implements AnnotatedElement {
		private Method readMethod;

		private Method writeMethod;

		private Field field;

		AnnotatedElementImpl(Method readMethod, Method writeMethod, Field field) {
			this.readMethod = readMethod;
			this.writeMethod = writeMethod;
			this.field = field;
		}

		public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
			if (writeMethod != null) {
				T ret = writeMethod.getAnnotation(annotationType);
				if (ret != null) {
					return ret;
				}
			}
			if (readMethod != null) {
				T ret = readMethod.getAnnotation(annotationType);
				if (ret != null) {
					return ret;
				}
			}
			if (field != null) {
				return field.getAnnotation(annotationType);
			}
			return null;
		}

		public Annotation[] getAnnotations() {
			List<Annotation> ret = new ArrayList<Annotation>();
			if (writeMethod != null) {
				ret.addAll(Arrays.asList(writeMethod.getAnnotations()));
			}
			if (readMethod != null) {
				ret.addAll(Arrays.asList(readMethod.getAnnotations()));
			}
			if (field != null) {
				ret.addAll(Arrays.asList(field.getAnnotations()));
			}
			return ret.toArray(new Annotation[ret.size()]);
		}

		public Annotation[] getDeclaredAnnotations() {
			List<Annotation> ret = new ArrayList<Annotation>();
			if (writeMethod != null) {
				ret.addAll(Arrays.asList(writeMethod.getDeclaredAnnotations()));
			}
			if (readMethod != null) {
				ret.addAll(Arrays.asList(readMethod.getDeclaredAnnotations()));
			}
			if (field != null) {
				ret.addAll(Arrays.asList(field.getDeclaredAnnotations()));
			}
			return ret.toArray(new Annotation[ret.size()]);
		}

		public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
			if (writeMethod != null) {
				boolean ret = writeMethod.isAnnotationPresent(annotationType);
				if (ret == true) {
					return ret;
				}
			}
			if (readMethod != null) {
				boolean ret = readMethod.isAnnotationPresent(annotationType);
				if (ret == true) {
					return ret;
				}
			}
			if (field != null) {
				return field.isAnnotationPresent(annotationType);
			}
			return false;
		}
	}
}
